# _*_ coding: utf-8 _*_
# .@FileName:Cmtest
# .@Data....:2025-04-23 : 00 : 51
# .@Aurhor..:LiuJingYu
"""
launch:
from __future__ import unicode_literals, print_function
import maya.cmds as mc
"""
import string
import maya.cmds as cmds

def create_unique_node(node_type, base_name):
    count = 0
    new_name = base_name
    while cmds.objExists(new_name):
        count += 1
        new_name = f"{base_name}_{count}"
    return cmds.createNode(node_type, name=new_name)


def make_spline(side, description, index, ctrl_num=3, jnt_num=5, control_mid=True):
    """
    create cMuscleSpline

    This script is simply convert from Maya muscle's mel script
    the mel script location is /Autodesk/Maya2020/scripts/muscle/cMuscleSplineUI.mel, line 660 cMS_makeSpline

    made some changes with the naming to fit current pipeline
    add some global offset attr so user can plug into global control to do some overall control

    Args:
        side(str): muscle spline's side, 'l', 'r' or 'm'
        description(str): muscle spline's description
        index(i): muscle spline's index
        ctrl_num(i): how many controls needed, default is 3
        jnt_num(i): how many joints needed, default is 5
        control_mid(bool): the start/end control will control in-between controls with falloff, default is True

    Returns:
        spline(str): cMuscleSpline node name
        controls(list): spline controls
        joints(list): spline joints
        group(str): root group for the spline setup
    """
    # make master groups
    base_grp_main_name = 'grp_{}_{}MusSpline_{:03d}'.format(side, description, index)
    grp_main = create_unique_node('transform', base_grp_main_name)

    # c muscle spline node
    base_spline_name = 'cMuscleSpline_{}_{}MusSpline_{:03d}Shape'.format(side, description, index)
    spline = create_unique_node('cMuscleSpline', base_spline_name)

    # lock label attrs
    cmds.setAttr(spline + '.DISPLAY', lock=True)
    cmds.setAttr(spline + '.TANGENTS', lock=True)
    cmds.setAttr(spline + '.LENGTH', lock=True)

    # get the transform node of spline shape and parent it under master group
    transform_spline = cmds.listRelatives(spline, parent=True)[0]
    cmds.parent(transform_spline, grp_main)
    # set inheritsTransform to 0 to avoid double transformation
    cmds.setAttr(transform_spline + '.inheritsTransform', 0)
    # lock the attrs because it shouldn't move
    for attr in 'trs':
        for axis in 'xyz':
            cmds.setAttr('{}.{}{}'.format(transform_spline, attr, axis), lock=True, keyable=False)
    # connect time to drive the spline
    cmds.connectAttr('time1.outTime', spline + '.inTime', force=True)
    # create some attrs so user can view easily in channel box
    cmds.addAttr(spline, longName='curLen', keyable=True)
    cmds.connectAttr(spline + '.outLen', spline + '.curLen')

    # add squash/stretch volume attr to connect with other controller
    cmds.addAttr(spline, longName='squashXMult', attributeType='float', minValue=0, defaultValue=1)
    cmds.addAttr(spline, longName='squashZMult', attributeType='float', minValue=0, defaultValue=1)
    cmds.addAttr(spline, longName='stretchXMult', attributeType='float', minValue=0, defaultValue=1)
    cmds.addAttr(spline, longName='stretchZMult', attributeType='float', minValue=0, defaultValue=1)

    # add global attr so it can be easily plugged with global control
    cmds.addAttr(spline, longName='jiggleMult', defaultValue=1)
    cmds.addAttr(spline, longName='jiggleXMult', defaultValue=1)
    cmds.addAttr(spline, longName='jiggleYMult', defaultValue=1)
    cmds.addAttr(spline, longName='jiggleZMult', defaultValue=1)
    cmds.addAttr(spline, longName='jiggleImpactMult', defaultValue=1)

    # ctrl grp
    grp_ctrls = cmds.createNode('transform',
                                name='grp_{}_{}MusSplineControls_{:03d}'.format(side, description, index))
    cmds.parent(grp_ctrls, grp_main)
    for attr in 'trs':
        for axis in 'xyz':
            cmds.setAttr('{}.{}{}'.format(grp_ctrls, attr, axis), lock=True, keyable=False)

    # driven grp for joints
    grp_driven = cmds.createNode('transform',
                                 name='grp_{}_{}MusSplineJoints_{:03d}'.format(side, description, index))
    cmds.parent(grp_driven, grp_main)
    cmds.setAttr(grp_driven + '.inheritsTransform', 0)
    for attr in 'trs':
        for axis in 'xyz':
            cmds.setAttr('{}.{}{}'.format(grp_driven, attr, axis), lock=True, keyable=False)

    ctrls = []
    # make controls
    for i in range(ctrl_num):
        ctrl_name = 'ctrl_{}_{}MusSpline{}_{:03d}'.format(side, description, string.ascii_uppercase[i], index)

        # create curve
        ctrl = cmds.curve(point=[[-0.5, -0.5, -0.5], [-0.5, -0.5, 0.5], [0.5, -0.5, 0.5], [0.5, -0.5, -0.5],
                                 [-0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [0.5, 0.5, -0.5], [0.5, -0.5, -0.5],
                                 [0.5, -0.5, 0.5], [0.5, 0.5, 0.5], [0.5, 0.5, -0.5], [0.5, 0.5, 0.5],
                                 [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, 0.5, -0.5]],
                          degree=1,
                          knot=[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0],
                          name=ctrl_name)
        # rename curve shape
        cmds.rename(cmds.listRelatives(ctrl, shapes=True)[0], ctrl_name + 'Shape')

        # add to list
        ctrls.append(ctrl)

        # make control hierarchy
        ctrl_nodes = []
        for node_type in ['zero', 'driven', 'connect', 'offset']:
            grp = cmds.createNode('transform', name=ctrl.replace('ctrl', node_type))
            ctrl_nodes.append(grp)

        # parent hierarchy
        cmds.parent(ctrl, ctrl_nodes[-1])
        cmds.parent(ctrl_nodes[-1], ctrl_nodes[-2])
        cmds.parent(ctrl_nodes[-2], ctrl_nodes[1])
        cmds.parent(ctrl_nodes[1], ctrl_nodes[0])

        # store zero node to variable
        zero = ctrl_nodes[0]

        # place controls vertically
        cmds.setAttr(zero + '.translateY', i)

        # parent zero group to control group
        cmds.parent(zero, grp_ctrls)

        # color control
        ctrl_shape = cmds.listRelatives(ctrl, shapes=True)[0]
        cmds.setAttr(ctrl_shape + '.overrideEnabled', 1)

        if side == 'l':
            color = 28
        elif side == 'r':
            color = 31
        else:
            color = 26
        cmds.setAttr(ctrl_shape + '.overrideColor', color)

        # add jiggle value, make sure start and end not jiggle
        if 0 < i < ctrl_num - 1:
            jiggle = 1
        else:
            jiggle = 0

        # add attrs
        cmds.addAttr(ctrl, longName='tangentLength', shortName='tanlen', minValue=0, defaultValue=1, keyable=True)
        cmds.addAttr(ctrl, longName='jiggle', shortName='jig', defaultValue=jiggle, minValue=0, keyable=True)
        cmds.addAttr(ctrl, longName='jiggleX', shortName='jigx', defaultValue=jiggle, minValue=0, keyable=True)
        cmds.addAttr(ctrl, longName='jiggleY', shortName='jigy', defaultValue=jiggle * 0.25, minValue=0, keyable=True)
        cmds.addAttr(ctrl, longName='jiggleZ', shortName='jigz', defaultValue=jiggle, minValue=0, keyable=True)
        cmds.addAttr(ctrl, longName='jiggleImpact', shortName='jigimp', defaultValue=jiggle * 0.5, minValue=0,
                     keyable=True)
        cmds.addAttr(ctrl, longName='jiggleImpactStart', shortName='jigimpst', defaultValue=1000, minValue=0,
                     keyable=True)
        cmds.addAttr(ctrl, longName='jiggleImpactStop', shortName='jigimpsp', defaultValue=0.001, minValue=0,
                     keyable=True)
        cmds.addAttr(ctrl, longName='cycle', shortName='cyc', minValue=1, defaultValue=12, keyable=True)
        cmds.addAttr(ctrl, longName='rest', shortName='rst', minValue=1, defaultValue=24, keyable=True)

        # lock hide scale and vis
        for attr in ['sx', 'sy', 'sz', 'v']:
            cmds.setAttr('{}.{}'.format(ctrl, attr), lock=True, keyable=False)

        # connect attr
        cmds.connectAttr(ctrl + '.worldMatrix[0]', '{}.controlData[{}].insertMatrix'.format(spline, i))

        # connect jiggle attr with global mult attr, then plug into controlData
        for attr in ['jiggle', 'jiggleX', 'jiggleY', 'jiggleZ', 'jiggleImpact']:
            # mult node
            mult = 'mult_{}_{}MusSpline{}{}{}_{:03d}'.format(side, description, string.ascii_uppercase[i],
                                                             attr[0].upper(), attr[1:], index)
            cmds.createNode('multDoubleLinear', name=mult)
            cmds.connectAttr('{}.{}'.format(ctrl, attr), mult + '.input1')
            cmds.connectAttr('{}.{}Mult'.format(spline, attr), mult + '.input2')

            # connect output to controlData
            cmds.connectAttr(mult + '.output', '{}.controlData[{}].{}'.format(spline, i, attr))

        # connect other attrs directly into controlData
        for attr in ['tangentLength', 'jiggleImpactStart', 'jiggleImpactStop', 'cycle', 'rest']:
            cmds.connectAttr('{}.{}'.format(ctrl, attr), '{}.controlData[{}].{}'.format(spline, i, attr))

    # control mid points
    if control_mid:
        for i in range(1, ctrl_num - 1):
            # get weight value
            weight = i / float(ctrl_num - 1)

            # get driven group
            driven = ctrls[i].replace('ctrl', 'driven')

            # point constraint with start and end
            pont_con = cmds.pointConstraint(ctrls[0], ctrls[-1], driven, maintainOffset=False)[0]
            cmds.setAttr('{}.{}W0'.format(pont_con, ctrls[0]), 1 - weight)
            cmds.setAttr('{}.{}W1'.format(pont_con, ctrls[-1]), weight)

            # aim constraint dummy node to start and end, and blend to ctrl's orient
            grp_aim_start = 'grp_{}_{}MusSpline{}AimStart_{:03d}'.format(side, description,
                                                                         string.ascii_uppercase[i], index)
            grp_aim_end = 'grp_{}_{}MusSpline{}AimEnd_{:03d}'.format(side, description,
                                                                     string.ascii_uppercase[i], index)

            cmds.createNode('transform', name=grp_aim_start)
            cmds.createNode('transform', name=grp_aim_end)

            # snap to control's position
            cmds.matchTransform(grp_aim_start, grp_aim_end, driven, pos=True, rot=True)
            # parent dummy groups under zero
            cmds.parent(grp_aim_start, grp_aim_end, driven.replace('driven', 'zero'))

            # aim constraint with start and end
            aim_start = cmds.aimConstraint(ctrls[0], grp_aim_start, aimVector=[0, -1, 0], upVector=[1, 0, 0],
                                           worldUpVector=[1, 0, 0], worldUpType='objectrotation',
                                           worldUpObject=ctrls[0], maintainOffset=False)[0]
            aim_end = cmds.aimConstraint(ctrls[-1], grp_aim_end, aimVector=[0, 1, 0], upVector=[1, 0, 0],
                                         worldUpVector=[1, 0, 0], worldUpType='objectrotation',
                                         worldUpObject=ctrls[-1], maintainOffset=False)[0]

            # add blend attr to chose either aim with x up or z up
            blend = 'blend_{}_{}MusSpline{}AimUp_{:03d}'.format(side, description,
                                                                string.ascii_uppercase[i], index)
            if not cmds.objExists(blend):
                cmds.createNode('blendColors', name=blend)
                cmds.connectAttr(spline + '.upAxis', blend + '.blender')
                cmds.setAttr(blend + '.color1', 0, 0, 1)
                cmds.setAttr(blend + '.color2', 1, 0, 0)

            # connect blend to aim constraint
            for attr in ['upVector', 'worldUpVector']:
                cmds.connectAttr(blend + '.output', '{}.{}'.format(aim_start, attr))
                cmds.connectAttr(blend + '.output', '{}.{}'.format(aim_end, attr))

            # connect point constraint's output to dummy groups since they are the same place with driver
            for attr in ['translateX', 'translateY', 'translateZ']:
                for grp in [grp_aim_start, grp_aim_end]:
                    cmds.connectAttr('{}.constraint{}{}'.format(pont_con, attr[0].upper(), attr[1:]),
                                     '{}.{}'.format(grp, attr))

            # orient constraint dummy groups with driver
            ont_con = cmds.orientConstraint(grp_aim_start, grp_aim_end, driven, maintainOffset=False)[0]
            cmds.setAttr(ont_con + '.interpType', 2)
            cmds.setAttr('{}.{}W0'.format(ont_con, grp_aim_start), 1 - weight)
            cmds.setAttr('{}.{}W1'.format(ont_con, grp_aim_end), weight)

    # create joints
    jnts = []
    for i in range(jnt_num):
        # get u value
        u_val = i / float(jnt_num - 1)

        # joint name
        jnt = 'jnt_{}_{}MusSpline{:03d}_{:03d}'.format(side, description, i + 1, index)

        # create joint
        cmds.createNode('joint', name=jnt)

        # append to jnts list
        jnts.append(jnt)

        # add uValue to joint
        cmds.addAttr(jnt, longName='uValue', minValue=0, maxValue=1, defaultValue=u_val, keyable=True)

        # parent to driven group
        cmds.parent(jnt, grp_driven)

        # connect attr
        cmds.connectAttr(jnt + '.uValue', '{}.readData[{}].readU'.format(spline, i))
        cmds.connectAttr(jnt + '.rotateOrder', '{}.readData[{}].readRotOrder'.format(spline, i))
        cmds.connectAttr('{}.outputData[{}].outTranslate'.format(spline, i), jnt + '.translate')
        cmds.connectAttr('{}.outputData[{}].outRotate'.format(spline, i), jnt + '.rotate')

        # add scale x, z
        # get scale weight, the middle point should be 1, and falloff till the start/end point
        mid_num = (jnt_num - 1) * 0.5
        scale_weight = 1 - (abs(mid_num - i) / float(mid_num))**2
        if scale_weight != 0:
            # not the start/end point

            # add scale x and scale z attr to spline so user can tweak individually
            cmds.addAttr(spline, longName='joint{:03d}SquashX'.format(i + 1), attributeType='float', minValue=0,
                         defaultValue=1 + scale_weight, keyable=True)
            cmds.addAttr(spline, longName='joint{:03d}StretchX'.format(i + 1), attributeType='float', minValue=0,
                         defaultValue=1 - scale_weight * 0.9, keyable=True)
            cmds.addAttr(spline, longName='joint{:03d}SquashZ'.format(i + 1), attributeType='float', minValue=0,
                         defaultValue=1 + scale_weight, keyable=True)
            cmds.addAttr(spline, longName='joint{:03d}StretchZ'.format(i + 1), attributeType='float', minValue=0,
                         defaultValue=1 - scale_weight * 0.9, keyable=True)

            # add blend node to blend squash stretch
            blend_squash = 'blend_{}_{}MusSplineSquash{:03d}_{:03d}'.format(side, description, i + 1, index)
            blend_stretch = 'blend_{}_{}MusSplineStretch{:03d}_{:03d}'.format(side, description, i + 1, index)

            cmds.createNode('blendColors', name=blend_squash)
            cmds.createNode('blendColors', name=blend_stretch)

            # connect blender with spline's outPctSquash/Stretch
            cmds.connectAttr(spline + '.outPctSquash', blend_squash + '.blender')
            cmds.connectAttr(spline + '.outPctStretch', blend_stretch + '.blender')
            cmds.setAttr(blend_squash + '.color2', 1, 1, 1)
            cmds.setAttr(blend_stretch + '.color2', 1, 1, 1)

            # mult node to multiply individual joint squash and stretch with global mult
            mult_squash = 'mult_{}_{}MusSplineSquash{:03d}_{:03d}'.format(side, description, i + 1, index)
            mult_stretch = 'mult_{}_{}MusSplineStretch{:03d}_{:03d}'.format(side, description, i + 1, index)

            cmds.createNode('multiplyDivide', name=mult_squash)
            cmds.createNode('multiplyDivide', name=mult_stretch)

            cmds.connectAttr('{}.joint{:03d}SquashX'.format(spline, i + 1), mult_squash + '.input1X')
            cmds.connectAttr('{}.joint{:03d}SquashZ'.format(spline, i + 1), mult_squash + '.input1Z')
            cmds.connectAttr('{}.joint{:03d}StretchX'.format(spline, i + 1), mult_stretch + '.input1X')
            cmds.connectAttr('{}.joint{:03d}StretchZ'.format(spline, i + 1), mult_stretch + '.input1Z')
            cmds.connectAttr(spline + '.squashXMult', mult_squash + '.input2X')
            cmds.connectAttr(spline + '.squashZMult', mult_squash + '.input2Z')
            cmds.connectAttr(spline + '.stretchXMult', mult_stretch + '.input2X')
            cmds.connectAttr(spline + '.stretchZMult', mult_stretch + '.input2Z')

            # connect to blend colors input1X
            cmds.connectAttr(mult_squash + '.output', blend_squash + '.color1')
            cmds.connectAttr(mult_stretch + '.output', blend_stretch + '.color1')

            # plus node to add scale together
            plus = 'plus_{}_{}MusSplineSquashStretch{:03d}_{:03d}'.format(side, description, i + 1, index)
            cmds.createNode('plusMinusAverage', name=plus)
            cmds.connectAttr(blend_squash + '.output', plus + '.input3D[0]')
            cmds.connectAttr(blend_stretch + '.output', plus + '.input3D[1]')
            cmds.setAttr(plus + '.input3D[2]', -1, -1, -1)

            # multiply node to mult global scale
            mult_scale = 'mult_{}_{}MusSplineScale{:03d}_{:03d}'.format(side, description, i + 1, index)
            cmds.createNode('multiplyDivide', name=mult_scale)
            cmds.connectAttr(plus + '.output3D', mult_scale + '.input1')
            cmds.connectAttr(spline + '.userScale', mult_scale + '.input2X')
            cmds.connectAttr(spline + '.userScale', mult_scale + '.input2Y')
            cmds.connectAttr(spline + '.userScale', mult_scale + '.input2Z')

            # connect back to joint
            cmds.connectAttr(mult_scale + '.outputX', jnt + '.scaleX')
            cmds.connectAttr(mult_scale + '.outputZ', jnt + '.scaleZ')
            cmds.connectAttr(spline + '.userScale', jnt + '.scaleY')

        else:
            cmds.connectAttr(spline + '.userScale', jnt + '.scaleX')
            cmds.connectAttr(spline + '.userScale', jnt + '.scaleY')
            cmds.connectAttr(spline + '.userScale', jnt + '.scaleZ')

    # set squash stretch values
    # get length
    length = cmds.getAttr(spline + '.outLen')
    # set default length
    cmds.setAttr(spline + '.lenDefault', length)
    # stretch and squash
    cmds.setAttr(spline + '.lenSquash', length * 0.25)
    cmds.setAttr(spline + '.lenStretch', length * 2.5)

    return spline, ctrls, jnts, grp_main


def add_spline(mus_joint, joint_number=5, control_number=3, control_mid=True, mirror=True, scale_attr=None):
    """
    add cMuscle spline setup to given muscle joint

    Args:
        mus_joint (str): muscle joint need to add cMuscle spline setup
        joint_number (int): cMuscle spline joints number
        control_number (int): cMuscle spline controls number
        control_mid (bool): constraint the mid controller with start/end controller, default is True
        mirror (bool): mirror to the right side
        scale_attr (str): global scale attribute
    """
    # get name parts
    name_parts = mus_joint.split('_')
    if len(name_parts) < 4:
        print(f"Error: Invalid muscle joint name {mus_joint}")
        return
    side = name_parts[1]
    description = name_parts[2]
    try:
        index = int(name_parts[3])
    except ValueError:
        print(f"Error: Invalid index in {mus_joint}")
        return

    # get muscle info
    mus_group_list = cmds.listRelatives(mus_joint, parent=True)
    if not mus_group_list:
        print(f"Error: No parent found for {mus_joint}")
        return
    mus_group = mus_group_list[0]

    end_joint_list = cmds.listRelatives(mus_joint, children=True, type='joint')
    if not end_joint_list:
        print(f"Error: No child joint found for {mus_joint}")
        return
    end_joint = end_joint_list[0]

    aim_locator = 'loc_{}_{}AimTarget_{:03d}'.format(side, description, index)
    grp_locator = aim_locator.replace('loc_', 'driven_')
    pnt_con_list = cmds.listConnections(grp_locator, source=True, destination=False, plugs=False)
    if not pnt_con_list:
        print(f"Error: No connections found for {grp_locator}")
        return
    pnt_con = pnt_con_list[0]

    driver_node_list = cmds.listConnections(pnt_con + '.target[0].targetTranslate', source=True, destination=False,
                                            plugs=False)
    if not driver_node_list:
        print(f"Error: No driver node found for {pnt_con}")
        return
    driver_node = driver_node_list[0]

    # get world up vector
    aim_vector = [1, 0, 0]
    world_up_vector = [0, 1, 0]
    if side == 'r':
        aim_vector = [-1, 0, 0]
        world_up_vector = [0, -1, 0]

    # create cMuscle spline
    spline_node, ctrls, jnts, grp_spline = make_spline(side, description, index, ctrl_num=control_number,
                                                       jnt_num=joint_number, control_mid=control_mid)

    # create pos group to present muscle joint's position
    grp_pos = 'zero_{}_{}MusSpline_{:03d}'.format(side, description, index)
    if cmds.objExists(grp_pos):
        print(f"Node {grp_pos} already exists. Renaming...")
        grp_pos = cmds.rename(grp_pos, grp_pos + "_new")
    cmds.createNode('transform', name=grp_pos)
    cmds.matchTransform(grp_pos, mus_joint, position=True, rotation=True)
    cmds.parent(grp_pos, mus_group)

    # connect with muscle joint's position
    cmds.connectAttr(mus_joint + '.translate', grp_pos + '.translate')
    cmds.connectAttr(mus_joint + '.rotate', grp_pos + '.rotate')

    # parent to muscle joint
    cmds.parent(grp_spline, grp_pos)
    # snap the position
    cmds.matchTransform(grp_spline, grp_pos, position=True, rotation=True)
    # do a temp aim constraint to orient the group
    cmds.delete(cmds.aimConstraint(end_joint, grp_spline, aimVector=[0, 1, 0], upVector=[-1, 0, 0],
                                   worldUpType='objectrotation', worldUpObject=mus_joint,
                                   worldUpVector=world_up_vector, maintainOffset=False))

    # snap each control to its position
    # snap the end control
    cmds.matchTransform(ctrls[-1].replace('ctrl', 'zero'), end_joint, position=True, rotation=False)

    # snap the in-between so driven node will get lesser offset value
    for i in range(1, control_number - 1):
        # get weight value
        weight = i / float(control_number - 1)

        # get zero group
        zero = ctrls[i].replace('ctrl', 'zero')

        # point constraint with start and end
        pnt_con = cmds.pointConstraint(ctrls[0], ctrls[-1], zero, maintainOffset=False)[0]
        cmds.setAttr('{}.{}W0'.format(pnt_con, ctrls[0]), 1 - weight)
        cmds.setAttr('{}.{}W1'.format(pnt_con, ctrls[-1]), weight)

        # delete point constraint
        cmds.delete(pnt_con)

    # point constraint end control driven with muscle joint's end joint
    ctrl_end_driven = ctrls[-1].replace('ctrl', 'driven')
    cmds.pointConstraint(aim_locator, ctrl_end_driven, maintainOffset=False)

    # reverse aim set up for end control
    aim_node = cmds.createNode('transform', name='grp_{}_{}EndAim_{:03d}'.format(side, description, index))
    zero_aim_node = cmds.createNode('transform', name=aim_node.replace('grp', 'zero'))
    cmds.parent(aim_node, zero_aim_node)

    cmds.matchTransform(zero_aim_node, end_joint, position=True)

    # parent to driver node
    cmds.parent(zero_aim_node, driver_node)

    # temp aim back to muscle group to get orientation
    cmds.delete(cmds.aimConstraint(mus_joint, zero_aim_node, maintainOffset=False,
                                   aimVector=aim_vector, upVector=world_up_vector,
                                   worldUpType='objectrotation', worldUpVector=world_up_vector))

    cmds.pointConstraint(aim_locator, aim_node, maintainOffset=False)
    cmds.aimConstraint(mus_group, aim_node, maintainOffset=False, aimVector=aim_vector,
                       upVector=world_up_vector, worldUpType='none')

    # add offset transform
    offset_grp = cmds.createNode('transform', name='grp_{}_{}EndAimOffset_{:03d}'.format(side, description, index))
    cmds.parent(offset_grp, aim_node)
    cmds.matchTransform(offset_grp, ctrl_end_driven, position=True, rotation=True)
    # orient constraint with end control's driven node
    cmds.orientConstraint(offset_grp, ctrl_end_driven, maintainOffset=False)

    # reset curve length and stretch squash limit
    # get default curve length
    if cmds.objExists(spline_node + '.curLen'):
        crv_length = cmds.getAttr(spline_node + '.curLen')
        # set as default
        cmds.setAttr(spline_node + '.lenDefault', crv_length)
        # set stretch and squash limit
        cmds.setAttr(spline_node + '.lenStretch', crv_length * 2)
        cmds.setAttr(spline_node + '.lenSquash', crv_length * 0.5)
    else:
        print(f"Error: Attribute {spline_node}.curLen does not exist.")

    # connect global scale
    if scale_attr and cmds.objExists(scale_attr) and cmds.attributeQuery('userScale', node=spline_node, exists=True):
        cmds.connectAttr(scale_attr, spline_node + '.userScale')
    else:
        print(f"Error: Cannot connect {scale_attr} to {spline_node}.userScale")

    # hide muscle joints
    cmds.setAttr(mus_joint + '.visibility', 0)

    if mirror and side == 'l':
        add_spline(mus_joint.replace('_l_', '_r_'), joint_number=joint_number, control_number=control_number,
                   control_mid=control_mid, mirror=False, scale_attr=scale_attr)


def mirror_muscle_spline_settings():
    """
    mirror cMuscleSpline settings from left to right
    """
    # get all left side muscle spline node and muscle spline controllers
    spline_nodes = cmds.ls('cMuscleSpline_l_*MusSpline_???Shape')
    if not spline_nodes:
        print("No left side muscle spline nodes found.")
        return

    spline_ctrls = cmds.ls('ctrl_l_*MusSpline?_???')
    if not spline_ctrls:
        print("No left side muscle spline controllers found.")
        return

    # loop in each node
    for node in spline_nodes + spline_ctrls:
        node_right = node.replace('_l_', '_r_')
        # get all custom attrs
        attrs = cmds.listAttr(node, userDefined=True)
        if not attrs:
            continue

        for attr in attrs:
            # check if it has connection or locked
            node_attr = '{}.{}'.format(node, attr)
            has_connection = (cmds.listConnections(node_attr, source=True, destination=False) or
                              cmds.listConnections(node_attr, source=False, destination=True))
            if not has_connection and not cmds.getAttr(node_attr, lock=True):
                try:
                    val = cmds.getAttr(node_attr)
                    # 假设某些属性值需要取反
                    if attr in ['some_attr_that_needs_mirroring']:
                        val = -val
                    if cmds.objExists(node_right) and cmds.attributeQuery(attr, node=node_right, exists=True):
                        cmds.setAttr('{}.{}'.format(node_right, attr), val)
                    else:
                        print(f"Node {node_right} or attribute {attr} does not exist.")
                except Exception as e:
                    print(f"Error getting or setting attribute {node_attr}: {e}")
